import logging
import math
from pathlib import Path
from timeit import default_timer as timer

from utils import box_iou, xyxy2xywh


class Analyzer:
    def __init__(self):
        self.infer_type = None
        self.stream_type = None
        self.time_threshold = 0
        self.error_threshold = 0
        self.obj_exist_times = []
        self.obj_present_serises = []
        self.obj_coods_list = []

    def set_infer_args(self, infer_args):
        logging.warning(f"Infer type is set to {infer_args['type']}.")
        self.infer_type = infer_args["type"]
        self.time_threshold = infer_args["time_threshold"]
        self.error_threshold = infer_args["error_threshold"]
        self.obj_exist_times = []
        self.obj_present_serises = []
        self.obj_coods_list = []
        if infer_args["type"].startswith("rtsp"):
            self.stream_type = "rtsp"
        elif Path(infer_args["type"]).is_file():
            self.stream_type = "file"

    def remove_obj(self, index):
        self.obj_exist_times.pop(index)
        self.obj_present_serises.pop(index)
        self.obj_coods_list.pop(index)

    def add_obj(self, obj):
        self.obj_exist_times.append(ObjTimeLine(self.time_threshold, self.stream_type))
        self.obj_present_serises.append("1")
        self.obj_coods_list.append(obj[:4])

    def analyze(self, objs, time_cost, frame):
        # step 1
        added_objs = []
        if len(self.obj_coods_list):
            for index, obj_old in enumerate(self.obj_coods_list):
                for new_index, obj_new in enumerate(objs):
                    if box_iou(obj_old, obj_new[:4]) > 0.75:
                        self.obj_present_serises[index] += "1"
                        self.obj_coods_list[index] = obj_new[:4]
                        if obj_new[4] == self.infer_type:
                            self.obj_exist_times[index].update(time_cost)
                        added_objs.append(new_index)
                        break
                else:
                    self.obj_present_serises[index] += "0"

        for index, obj in enumerate(objs):
            if index not in added_objs:
                self.add_obj(obj)
        # step 2
        # obj_list_length = len(self.obj_coods_list)
        for index in range(len(self.obj_coods_list) - 1, -1, -1):
            if self.obj_exist_times[
                index
            ].total_time() < self.error_threshold and self.obj_present_serises[
                index
            ].endswith(
                "0"
            ):
                self.remove_obj(index)

        # Step 3
        alert = False
        xyxy = None
        if not len(self.obj_coods_list):
            return False, None
        for index in range(len(self.obj_coods_list)):
            if (
                self.infer_type == "touch"
                and self.obj_present_serises[index].endswith("00000")
                and self.obj_exist_times[index].total_time() < self.time_threshold * 0.9
            ) or (
                self.infer_type == "sleep"
                and self.obj_exist_times[index].total_time_in_interval()
                > self.time_threshold * 0.8
            ):
                alert = True
                xyxy = self.obj_coods_list[index]
                logging.info(f"Illegal behavior detected. Type: {self.infer_type}")
            if self.obj_present_serises[index].endswith("00000") or alert:
                self.remove_obj(index)
                return alert, xyxy
        else:
            return False, None


class ObjTimeLine:
    def __init__(self, time_threshold, stream_type):
        # time_line: time point serise of each time update() is called.
        # time_costs: the value of self.time_costs which passed by update().
        # the value of them is one-to-one correspondence.
        self.time_line = []
        self.time_costs = []
        self.time_threshold = time_threshold
        self.stream_type = stream_type
        self._total_time = 0

    def update(self, time_cost):
        self._total_time += time_cost
        self.time_line.append(timer())
        self.time_costs.append(time_cost)
        time_now = timer()
        while self.time_line[0] < time_now - self.time_threshold:
            self.time_line.pop(0)
            self.time_costs.pop(0)

        # Avoid total_time out of bounds while detecting type is sleep.
        self._total_time %= 3600

    # total time in the time interval, used for sleep detection.
    def total_time_in_interval(self):
        if self.stream_type == "rtsp":
            return sum(self.time_costs)
        else:
            return self._total_time

    # total time, used for touch detection.
    def total_time(self):
        return self._total_time


class TouchAnalyzer:
    def __init__(self):
        self.infer_type = None
        self.stream_type = None
        self.time_threshold = 0
        self.error_threshold = 0
        self.obj_exist_time = 0
        self.obj_present_serise = ""
        self.frame_count = 0
        self.person_coods = None
        self.error_frame = None

    def set_infer_args(self, infer_args):
        logging.info(f"Infer type is set to {infer_args['type']}.")
        self.infer_type = infer_args["type"]
        self.time_threshold = infer_args["time_threshold"]
        self.error_threshold = infer_args["error_threshold"]
        self.reset_analyzer()
        if infer_args["type"].startswith("rtsp"):
            self.stream_type = "rtsp"
        elif Path(infer_args["type"]).is_file():
            self.stream_type = "file"

    def reset_analyzer(self):
        self.obj_present_serise = ""
        self.obj_exist_time = 0
        self.frame_count = 0
        self.person_coods = None

    def analyze(self, objs, time_cost, frame):
        res, ball, persons = self.get_objs(objs)
        if not res:
            return False, None
        self.frame_count += 1
        if self.obj_present_serise:
            self.obj_exist_time += time_cost
        status = self.get_status(objs)
        if status:
            self.obj_present_serise += "1"
            if not self.person_coods:
                self.person_coods = self.get_nearest_person(ball, persons)
                self.error_frame = frame
        elif self.obj_present_serise:
            self.obj_present_serise += "0"

        # remove environment noise
        if (
            self.obj_exist_time < self.error_threshold
            and self.obj_present_serise.endswith("0")
        ):
            self.reset_analyzer()

        # Step 3
        alert = False
        if (
            self.obj_present_serise.endswith("000000")
            and self.obj_exist_time < self.time_threshold
        ):
            alert = True
            logging.info(
                f"Illegal behavior detected. Type: {self.infer_type}. "
                f"Exist frame: {self.frame_count}. "
            )
        xyxy = self.person_coods
        if self.obj_present_serise.endswith("000000") or alert:
            self.reset_analyzer()
        return alert, xyxy

    def get_objs(self, objs):
        ball = [x[:4] for x in objs if x[4] == "touch" or x[4] == "untouch"]
        persons = [x[:4] for x in objs if x[4] == "person"]
        if not persons or not ball:
            return False, None, None
        return True, ball[0], persons

    def get_error_frame(self):
        return self.error_frame

    def get_nearest_person(self, ball, persons):
        nearest_person = None
        min_dist = 8294400
        ball_point = ((ball[0] + ball[2]) / 2, ball[1])
        for person in persons:
            person_center = xyxy2xywh(person)[:2]
            this_dist = math.dist(person_center, ball_point)
            if this_dist < min_dist:
                min_dist = this_dist
                nearest_person = person
        return nearest_person

    def get_status(self, objs):
        for obj in objs:
            if obj[4] == self.infer_type:
                return True
        return False
